package org.joa.actor;

import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

import org.joa.actor.transaction.Transaction;
import org.joa.manager.ActorManager;
import org.joa.manager.ManagedActor;
import org.joa.manager.exception.TransactionLockTimeoutException;
import org.joa.util.Callback;

public class ActorProxy implements ActorProxyInterface, InvocationHandler {
    protected ActorName remote;
    protected transient ActorName owner;
    protected transient Class<?> interfaceClass;

    protected transient ActorManager actorManager = ActorManager.instance;

    public ActorProxy(ActorName remote, ActorName owner, Class<?> interfaceClass) {
        this.remote = remote;
        this.owner = owner;
        this.interfaceClass = interfaceClass;
    }

    public ActorProxy create(ActorName remote, ActorName owner) {
        try {
            Constructor<? extends ActorProxy> ctor =
                getClass().getConstructor(ActorName.class, ActorName.class);
            return ctor.newInstance(new Object[] { remote, owner });
        } catch (Exception e) {
            // This should never happen
            throw new RuntimeException(e);
        }
    }

    @SuppressWarnings("unchecked")
    public Object invoke(final Object proxy, Method method, Object[] args) throws Throwable {

        if (method.getName().equals("getRemoteActorName")) {
            return getRemoteActorName();
        } else if (method.getName().equals("getProxyInterfaceClass")) {
            return getProxyInterfaceClass();
        } else if (method.getName().equals("setOwnerActorName")) {
            setOwnerActorName((ActorName) args[0]);
            return null;
        } else if (method.getName().equals("toString")) {
            return toString();
        }

        // Get current transaction from owner actor
        ManagedActor actor = actorManager.getActor(owner);
        final Transaction transaction = actor.getCurrentTransaction();
        if (transaction != null && !(proxy instanceof Transaction)) {
            // A response is pending from remote
            transaction.incrementPendingMessageCount(remote);
        }

        if (args != null && args.length > 0 && (args[args.length - 1]) instanceof Callback<?>) {
            // If the method call is asynchronous
            Object callbackObj = args[args.length - 1];
            final Callback<Serializable> callback = (Callback<Serializable>) callbackObj;
            Serializable[] newArgs = createSerializableArgs(args, 0, args.length - 1);

            actorManager.invoke(
                remote,
                method.getName(),
                newArgs,
                transaction,
                new Callback<Serializable>() {
                    @Override
                    public void onSuccess(Serializable result) {
                        if (transaction != null && !(proxy instanceof Transaction)) {
                            // Response received from remote
                            transaction.decrementPendingMessageCount(remote);
                        }

                        callback.onSuccess(result);
                    }

                    @Override
                    public void onError(Throwable exception) {
                        if (transaction != null && !(proxy instanceof Transaction)) {
                            if (exception instanceof TransactionLockTimeoutException) {
                                transaction.retry();
                            } else {
                                transaction.rollback();
                            }
                        }

                        if (transaction != null && !(proxy instanceof Transaction)) {
                            // Response received from remote
                            transaction.decrementPendingMessageCount(remote);
                        }

                        callback.onError(exception);
                    }
                });

            return null;
        } else {
            // If the method call is synchronous
            Serializable[] newArgs = createSerializableArgs(args);

            try {
                Serializable result =
                    actorManager.invoke(remote, method.getName(), newArgs, transaction);

                return result;
            } catch (Throwable ex) {
                if (transaction != null && !(proxy instanceof Transaction)) {
                    if (ex instanceof TransactionLockTimeoutException) {
                        transaction.retry();
                    } else {
                        transaction.rollback();
                    }
                }
                throw ex;
            } finally {
                if (transaction != null && !(proxy instanceof Transaction)) {
                    transaction.decrementPendingMessageCount(remote);
                }
            }
        }
    }

    protected Serializable[] createSerializableArgs(Object args[]) {
        if (args == null) {
            return null;
        }

        return createSerializableArgs(args, 0, args.length);
    }

    protected Serializable[] createSerializableArgs(Object args[], int start, int length) {
        if (args == null) {
            return null;
        }

        Serializable[] newArgs = new Serializable[length];
        for (int idx = 0; idx < length; idx++) {
            Object arg = args[idx + start];

            if (arg == null || arg instanceof Serializable) {
                newArgs[idx] = (Serializable) arg;
            } else {
                throw new RuntimeException("Parameter of type " + arg + " was not serializable");
            }
        }

        return newArgs;
    }

    @Override
    public ActorName getRemoteActorName() {
        return remote;
    }

    @Override
    public Class<?> getProxyInterfaceClass() {
        return interfaceClass;
    }

    @Override
    public String toString() {
        return remote.toString() + " {proxy}";
    }

    @Override
    public void setOwnerActorName(ActorName owner) {
        this.owner = owner;
    }
}
